feat(spec): extend the ownership enum with ADR-0117 D1's business_unit tier (#5678) - #7260
Conversation
…it` tier (#5678) `ObjectSchema.ownership` becomes `'user' | 'business_unit' | 'org' | 'none'`. The fourth tier means "owned by an org UNIT, not by a person": `owning_business_unit_id` is injected, `owner_id` deliberately is not — D1's table, end to end. This is the DECLARATION surface only. #5677 already landed the execution surface: `applySystemFields`' owner decision became an allow-list and the shared derivation `resolveInjectedSystemColumns` already implements D1's row. The ordering was the point — under the previous deny-list a fourth value would have fallen through and been stamped `owner_id`, the exact inverse of the tier's meaning — so this change is strictly after it. The #4611 pin is REWRITTEN, not deleted, exactly as it asked to be: it now asserts the fourth value is ACCEPTED and resolves to D1's row against the injection authority, plus a second pin that a FIFTH value is still rejected and the rejection enumerates all four legal values. Consumption radius a spec-only scan misses: `os explain object`'s schema catalog is hand-maintained (`packages/cli/src/commands/explain.ts`) and its token-set assertion pins the enum EXACTLY, so it is co-updated here. D2 (stamping policy), D4 (transfer guard), D5 (legal-entity resolution) and D8 (enablement gate) remain undecided in ADR-0117 and are untouched: the column stays provisioned-but-inert, so the tier is declarable and nothing stamps it yet. Closes #5678 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQSFSfxwBGQcMyJddGrWfx
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
一处冲突:`scripts/adr-anchors.json` —— main 的 #7301 把 ADR 锚点注册表 按文件分片(`scripts/adr-anchors/*.json`)并删除了整块 JSON,本分支同时 改了其中两条锚点的内容。按**并集**处置,不是二选一:采用 main 的分片布局, 把两条锚点的新内容移植进对应分片。 - `packages__objectql__src__registry.ts.json` —— invariant 由「D9 是 DESIGN ONLY,不要实现」改写为已落地的模型 - `packages__metadata-protocol__src__protocol.ts.json` —— 依 D9 §8 加上 ADR-0029;移植前逐字节校验了 main 侧 ADR-0119 那句未变,确保是并集而非覆盖 生成物 `content/docs/references/**` 取 main 一侧后用 `gen:schema` + `gen:docs` 重新生成,未手工合并。 其余全部自动合并。main 的 #7260 把**记录**所有权枚举扩成 `user | business_unit | org | none`,与 D9 的**贡献**种类 (`own | extend | overlay`)是两个枚举,两侧改动均完整保留。 main 对 `registry.ts` 的改动是 `applySystemFields` 内的纯注释,与 D9 模型 无交集;`registerObject` / `resolveObject` / `getArtifactItem` / 两个 hydration 缝 / delete heal 均未被 main 触碰。任何钉子都未移动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
What
ObjectSchema.ownershipbecomes'user' | 'business_unit' | 'org' | 'none'. The fourth tier means owned by an org UNIT, not by a person:owning_business_unit_idis injected,owner_iddeliberately is not.This is the D1 declaration surface only. Per ADR-0117 D1's table:
ownershipowner_idowning_business_unit_id'user'/ omitted'business_unit'(new)'org'/'none'Authorization for this scope: the 2026-08-09 unlock sweep — both blockers resolved, the 「严格后置于 #5677」 ordering satisfied on
main, and "the work plan stands as written".Premise re-measurement (on fresh
origin/main@f40c5b4)Every premise the card rests on was re-measured before the first edit, not assumed:
'business_unit'packages/spec/src/data/object.zod.ts(the site had drifted from the card's:1152to:1393)z.enum(['user', 'org', 'none'])packages/objectql/src/registry.ts:377-394readsplan.owneroff a POSITIVE list;resolveInjectedSystemColumns(packages/spec/src/data/injected-system-columns.ts:171-172) implementsowner = undefined || 'user'andowningBusinessUnit = owner || 'business_unit'SystemFieldName.OWNING_BUSINESS_UNIT_IDregistered; ADR-0117 on disk asAccepted (D1/D3 scoped)packages/spec/src/data/object.test.ts:1020—it('rejects business_unit until ADR-0117 D1 injection lands, naming the three legal values')So the ordering this card exists to respect held: the engine honours the tier, and only the acceptance surface was missing.
Changes
packages/spec/src/data/object.zod.tserrorstring anddescribe()(both of which spell the legal values out verbatim); theownershipJSDoc; thesystemFieldsJSDoc'sowner_id/owning_business_unit_idbullets; thesystemFields.ownerguidance string (a third value now skipsowner_id, for a different reason than the other two)packages/spec/src/data/object.test.tspackages/cli/src/commands/explain.tsos explain object's hand-maintained catalog — type string + descriptionpackages/cli/test/commands.test.tspackages/spec/src/system/constants/system-names.tsOWNING_BUSINESS_UNIT_IDJSDoc — the "not authorable yet" paragraph, named as a co-update target by the pin itself; andOWNER_ID's one-line withheld-under listpackages/spec/src/data/injected-system-columns.tsstring-widening rationale now stands on its real reason (the function takesunknown, incl. pre-parse input) rather than on the closed gappackages/objectql/src/registry.tspackages/objectql/src/registry.test.tsas anycasts the old comment predicted would become droppable — dropped forServiceObject/satisfies ServiceObject, no assertion changedpackages/spec/src/data/object-strictness-batch20.test.tsbusiness_unitsplits them, asserted against the authoritypackages/spec/src/system/constants/system-names.test.tspackages/metadata-protocol/src/protocol.injected-system-columns.test.tsbusiness_unitrow to the #6562 served-columns table — the read surface must reportowner_idas absent on a unit-owned objectcontent/docs/references/data/object.mdxgen:docs) — exactly one line, from thedescribe()content/docs/data-modeling/objects.mdx,skills/objectstack-data/SKILL.md,packages/spec/liveness/object.json.changeset/ownership-business-unit-enum-member.md@objectstack/specminor (author-visible enum member) +@objectstack/clipatchThe #4611 pin rewrite — reverse verification, direction predicted first
The old pin named its own expiry: "when #5678 arrives, this test failing is the intended signal to REWRITE it (not to delete the guard) — assert the fourth value is accepted and that a fifth is still rejected naming four legal values." Done exactly that, and the flipped pin was measured in both directions after stating the prediction:
git checkout origin/main -- packages/spec/src/data/object.zod.ts, re-runcreatethrows onbusiness_unit; the rejection message enumerates only["user","org","none"], sotoContain('business_unit')failspackages/clitoken-set pin is RED on the CLI revert alone — i.e. the spec change does not carry itgit checkout origin/main -- packages/cli/src/commands/explain.ts, re-runSet{none,org,user}≠Set{business_unit,none,org,user}B is the card's "a spec-only scan would miss it" claim, measured rather than asserted:
explain.tsis hand-maintained and does not derive from the enum, so nothing inpackages/specmoves it. Its comment now records that, so the next enum change finds the reason instead of rediscovering it.The rewritten pin also asserts the tier resolves to D1's row against
resolveInjectedSystemColumns— the authority, not prose — so the enum member cannot drift away from what the engine does with it.Gates
pnpm --filter @objectstack/spec build(first, per the stale-dist trap #7122)check:generated— all 11 artifactscheck:docswas the only stale one;gen:docs→ 1 line)@objectstack/spectests@objectstack/objectqltests@objectstack/clitests@objectstack/metadata-protocolteststypecheck(spec + objectql + cli)check:empty-changeset,check:changeset-no-major,check:adr-0087-registrationSpecial-inspection items
check:api-surfaceandcheck:export-originsare green unchanged: the enum gains a member, not an export. Dual-snapshot was scoped but is not owed.authorable-surface.base.jsondid not move, and that is correct. The card flagged it as possibly legitimately moving. It is a flat list of key paths (data/Object:ownershipat:3559); a new enum member adds no key.check:authorable-surfacegreen with no diff.packages/spec/json-schema/**regenerated but is gitignored (.gitignore:61), so it is correctly absent from the diff despite carrying the new description.platform-objectsselect carries the record-ownership vocabulary (everyownershiphit there istransfer_ownership/ provider-ownership prose).ownership ===/.ownership:resolveInjectedSystemColumns(handles the tier since ADR-0117 D1 执行面:applySystemFields 的 wantOwner 由排除式翻为正面清单 + 注入 owning_business_unit_id #5677) andpackages/cli/src/commands/info.ts:79(display-only,obj.ownership || 'user'— prints the new value as-is).D2/D4/D5/D8 guard
No contact. Adding the enum member touches none of the four undecided items, and the hard stop was not reached:
owningBusinessUnit.policykey added; nothing stamps a value. The column stays provisioned-but-inert, so declaring the tier yields the column and the withheldowner_id, and nothing more.allowTransferreuse, no write-path change.The declaration/derivation boundary held cleanly: everything this PR changes is either the enum, prose describing it, or a test asserting the derivation that #5677 already landed.
docs/adr/**andcontent/docs/releases/are untouched.Closes #5678
Generated by Claude Code